/**
 * DitherFilter.java
 * 
 * @date: 2012-02-08
 * @author: Xiaoyu Guo (MUN# 200982155)
 * This file is part of the course project for COMP 6752, 
 * Application of Computer Graphics. All the file is written
 * by myself.
 */
package mun.cs6752.project.filter.dithering;

import java.awt.image.BufferedImage;

import mun.cs6752.project.filter.abstractClasses.AbstractImageFilter;
import mun.cs6752.project.filter.abstractClasses.AbstractImageFilterControl;
import mun.cs6752.project.filter.interfaces.IImageFilter;
import mun.cs6752.project.filter.interfaces.IImageFilterParameter;
import mun.cs6752.project.filter.interfaces.IImageProvider;

/**
 * @author Xiaoyu Guo
 *
 */
public class DitherFilter extends AbstractImageFilter implements IImageFilter {

    private BufferedImage destImage;

    /**
     * @param provider
     * @param name
     */
    public DitherFilter(IImageProvider provider) {
        super(provider, "Dither");
    }

    /* (non-Javadoc)
     * @see mun.cs6752.project.filter.interfaces.IImageFilter#execute(java.awt.image.BufferedImage, mun.cs6752.project.filter.interfaces.IImageFilterParameter)
     */
    @Override
    public BufferedImage execute(BufferedImage srcImage, IImageFilterParameter param) {
        this.cachedSrcImage = new BufferedImage(
            srcImage.getWidth(), 
            srcImage.getHeight(), 
            BufferedImage.TYPE_INT_RGB
        );
        
        for(int y = 0; y < srcImage.getHeight(); y++){
            for(int x = 0; x < srcImage.getWidth(); x++){
                cachedSrcImage.setRGB(x, y, srcImage.getRGB(x, y));
            }
        }
        
        this.destImage = new BufferedImage(
            srcImage.getWidth(), 
            srcImage.getHeight(), 
            BufferedImage.TYPE_INT_RGB
        );
        
        dither(cachedSrcImage, DISPERSED_SMALL);
        return destImage;
    }

    /* (non-Javadoc)
     * @see mun.cs6752.project.filter.interfaces.IImageFilter#getControl()
     */
    @Override
    public AbstractImageFilterControl getControl() {
        // TODO Auto-generated method stub
        return new AbstractImageFilterControl(provider, this){
            private static final long serialVersionUID = -627408798912327429L;

            @Override
            public IImageFilterParameter getParameter() {
                return null;
            }};
    }
    
    /** Class holding color diff between two pixels.
     * This class contains diff on all channels.
     * Because RGB information are 0-255 each, so diff information are -255-255
     * each, so 9 bits are required for each channel. It is not enough to store 
     * full diff information in original RGB color.
     */
    class Diff{
        int r;
        int g;
        int b;
        Diff(int r, int g, int b){
            this.r = r;
            this.g = g; 
            this.b = b;
        }
    }
    
    /** Perform channel diff between two images at given pixel
     * @param x x coord
     * @param y y coord
     * @return the diff between cachedSrcImage and destImage at given pixel
     */
    private Diff channelDiff(int x, int y){
        int srcRGB = cachedSrcImage.getRGB(x, y);
        int destRGB = destImage.getRGB(x, y);
        int diffR = getR(srcRGB) - getR(destRGB);
        int diffG = getG(srcRGB) - getG(destRGB);
        int diffB = getB(srcRGB) - getB(destRGB);
        return new Diff(diffR, diffG, diffB);
    }
    
    /** Adding the diff value into the given pixel on the specified image.
     * @param image the image to operate on 
     * @param x
     * @param y
     * @param diff the difference values
     * @param ratio the ratio in calculation. 1 means 1/16 will be added.
     */
    private void pixelAdd(BufferedImage image, int x, int y, Diff diff, int ratio){
        int destRGB = image.getRGB(x, y);
        int destR = getR(destRGB) + diff.r * ratio / 16;
        int destG = getG(destRGB) + diff.g * ratio / 16;
        int destB = getB(destRGB) + diff.b * ratio / 16;
        if(destR > 255){
            destR = 255;
        }
        if(destG > 255){
            destG = 255;
        }
        if(destB > 255){
            destB = 255;
        }
        
        destRGB = getRGB(destR, destG, destB);
        image.setRGB(x, y, destRGB);
    }
    
    /** Dither Matrix information */
    class DitherMatrix{
        private int[][] rawData;
        private int m;
        
        /** Construct a DitherMatrix using integer array */
        public DitherMatrix(int[][] rawData){
            if(rawData == null || rawData.length < 1){
                throw new IllegalArgumentException("Can not create matrix with empty data");
            }
            
            for(int i = 0; i < rawData.length; i++){
                if(rawData[i].length != rawData.length){
                    throw new IllegalArgumentException("Must be a square matrix");
                }
            }
            
            this.rawData = rawData;
            m = rawData.length;
        }
         
        /** Get the sampled value at given position */
        public int get(int x, int y){
            return Math.round( (float)rawData[x][y] * 256f / m / m );
        }
        
        /** Get the raw value at given position */
        public int getRaw(int x, int y){
            return rawData[x][y];
        }
        
        /** Get the size of the DitherMatrix */
        public int getSize(){
            return m;
        }
    }
    
    /** The 5x5 matrix used for Clustered Dithering */
    public final DitherMatrix CLUSTERED_SMALL = new DitherMatrix(
        new int[][]{
            {22,    14, 10, 17, 21  },
            {18,    6 , 2 , 5 , 13  },
            {11,    3 , 0 , 1 , 9   },
            {15,    7 , 4 , 8 , 20  },
            {23,    19, 12, 16, 24  }
        }
    );

    /** The 10x10 matrix used for Clustered Dithering */
    public final DitherMatrix CLUSTERED_LARGE = new DitherMatrix(
        new int[][]{
            {27,    35, 39, 32, 28, 22, 14, 10, 17, 21  },
            {31,    43, 47, 44, 36, 18, 6 , 2 , 5 , 13  },
            {38,    46, 49, 48, 40, 11, 3 , 0 , 1 , 9   },
            {34,    42, 45, 41, 29, 15, 7 , 4 , 8 , 20  },
            {26,    30, 37, 33, 25, 23, 19, 12, 16, 24  },
            
            {22,    14, 10, 17, 21, 27, 35, 39, 32, 28  },
            {18,    6 , 2 , 5 , 13, 31, 43, 47, 44, 36  },
            {11,    3 , 0 , 1 , 9 , 38, 46, 49, 48, 40  },
            {15,    7 , 4 , 8 , 20, 34, 42, 45, 41, 29  },
            {23,    19, 12, 16, 24, 26, 30, 37, 33, 25  }
        }
    );
    
    /** The 4x4 matrix used for Dispersed Dithering */
    public final DitherMatrix DISPERSED_SMALL = new DitherMatrix(
        new int[][]{
            {0 ,    8 ,     2 ,     10  },
            {12,    4 ,     14,     6   },
            {3 ,    11,     1 ,     9   },
            {15 ,   7 ,     13,     5   }
        }
    );

    /** The 8x8 matrix used for Dispersed Dithering */
    public final DitherMatrix DISPERSED_LARGE = new DitherMatrix(
        new int[][]{
            {0 ,    32, 8 , 40, 2 , 32, 10, 42  },
            {48,    16, 56, 24, 50, 18, 58, 26  },
            {12,    44, 4 , 36, 14, 46, 6 , 38  },
            {60,    28, 52, 20, 62, 30, 54, 22  },
            {3 ,    33, 11, 43, 1 , 31, 9 , 41  },
            {51,    19, 59, 27, 49, 17, 57, 25  },
            {15,    47, 7 , 39, 13, 45, 5 , 37  },
            {63,    31, 55, 23, 61, 29, 53, 21  }
        }
    );

    private BufferedImage cachedSrcImage;
    
    /** Perform dithering on the specified image using given matrix
     * @param srcImage the specified source image
     * @param matrix the dither matrix
     */
    private void dither(BufferedImage srcImage, DitherMatrix matrix){
        for(int y = 0; y < cachedSrcImage.getHeight(); y++){
            for(int x = 0; x < cachedSrcImage.getWidth(); x++){
                int q = y % matrix.getSize();
                int p = x % matrix.getSize();
                
                pixelThreshold(x, y, matrix.get(p, q), false);
            }
        }
    }
    

    /** Thresholding on one pixel and store the result in destImage
     * @param x the x axis
     * @param y the y axis
     * @param threshold the threshold value, range 0 - 255
     * @param useCache {@value true} if using cachedSrcImage, {@value false} if using srcImage.
     */
    private void pixelThreshold(int x, int y, int threshold, boolean useCache){
        int targetRGB;
        if(useCache){
            targetRGB = cachedSrcImage.getRGB(x, y);
        } else {
            targetRGB = provider.getSourceImage().getRGB(x, y);
        }
        int r = getR(targetRGB);
        int g = getG(targetRGB);
        int b = getB(targetRGB);
        
        r = (r > threshold) ? 255 : 0;
        g = (g > threshold) ? 255 : 0;
        b = (b > threshold) ? 255 : 0;
        
        targetRGB = getRGB(r, g, b);
        
        destImage.setRGB(x, y, targetRGB);
    }
}
